/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.jfonia.view.main;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.sound.midi.Instrument;
import org.jfonia.connect5.basics.BasicNode;
import org.jfonia.connect5.basics.Observer;
import org.jfonia.constants.ModelConstants;
import org.jfonia.model.IListObserver;
import org.jfonia.model.ListObservable;
import org.jfonia.model.Sequence;
import org.jfonia.model.Staff;
import org.jfonia.model.StaffCollection;
import org.jfonia.model.elements.Clef;
import org.jfonia.model.elements.TimeSignature;
import org.jfonia.model.elements.Tone;
import percussiongenerator.model.Track;
import percussiongenerator.model.TrackCollection;
import percussiongenerator.sound.IMidiPlayer;
import percussiongenerator.sound.ITimeListener;
import percussiongenerator.sound.MidiPlayerNoLoop;
import percussiongenerator.sound.SimpleInstrument;

/**
 *
 * @author Rik Bauwens
 */
public class LeadSheetPlayer extends BasicNode implements ITimeListener
{
        private static LeadSheetPlayer instance;

        private BasicNode stateObservable;
        private BasicNode playerStateObservable;

        private IMidiPlayer midiPlayer;
        private LeadSheet leadSheet;
        private Sequence<TimeSignature> timeSignatureSequence;
        private double beatsPerSecond;
        private boolean locked;
        private boolean leadSheetWaiting;

        private Map<String, Instrument> instrumentMap;
        private int currentTick;

        private List<ListObservable> listObservables;
        private IListObserver listObserver = new IListObserver()
        {
                public void itemAdded(Object item)
                {
                    new SetLeadSheet(leadSheet).start();
                        //setLeadSheet(leadSheet);
                }

                public void itemRemoved(Object item)
                {
                    new SetLeadSheet(leadSheet).start();
                        //setLeadSheet(leadSheet);
                }
        };

        private LeadSheetPlayer()
        {
                super();
                locked = true;
                leadSheetWaiting = false;
                stateObservable = new BasicNode();
                playerStateObservable = new BasicNode();
                instrumentMap = new HashMap<String, Instrument>();
                listObservables = new ArrayList<ListObservable>();
                new InitialiseMidiPlayer().start();
        }

        private class InitialiseMidiPlayer extends Thread
        {
                /**
                 * Procedure being called when the thread is started.
                 */
                @Override
                public void run()
                {
                        locked = true;
                        midiPlayer = new MidiPlayerNoLoop();
                        midiPlayer.addTimeListener(instance);
                        if(leadSheetWaiting)
                                new SetLeadSheet(leadSheet).start();
                        notifyStateObservers();
                        locked = false;
                }
        }

        public static synchronized LeadSheetPlayer getInstance()
        {
                if(instance == null)
                        instance = new LeadSheetPlayer();
                return instance;
        }

        public IMidiPlayer getMidiPlayer()
        {
                return midiPlayer;
        }

        public boolean isReady()
        {
                return midiPlayer != null && midiPlayer.isPlayerLoaded() && leadSheet != null;
        }

        public boolean isPlaying()
        {
                return midiPlayer.isPlaying();
        }

        public int getCurrentTick()
        {
                return currentTick;
        }

        public String[] getInstruments()
        {
                if(instrumentMap.isEmpty())
                        return null;
                String[] instruments = new String[instrumentMap.size()];
                return instrumentMap.keySet().toArray(instruments);
        }

        public String getInstrument(String instrument)
        {
                if(instrumentMap.containsKey(instrument))
                        return instrument;
                for(String currentInstrument : instrumentMap.keySet())
                {
                        if(currentInstrument.toLowerCase().contains(instrument.toLowerCase()))
                                return currentInstrument;
                }
                if(!instrumentMap.isEmpty())
                        return instrumentMap.keySet().iterator().next();
                return null;
        }

        public void setInstrument(String instrument)
        {
                if(midiPlayer == null || midiPlayer.getTrackCollection() == null)
                        return;

                if(!instrumentMap.containsKey(instrument))
                        return;

                for(Track track : midiPlayer.getTrackCollection().getTracks())
                        track.setInstrument(new SimpleInstrument(instrumentMap.get(instrument)));
        }

        public void play()
        {
                if(!locked && !isPlaying())
                {
                        midiPlayer.play();
                        notifyPlayerStateObservers();
                }
        }

        public void pause()
        {
                if(!locked && isPlaying())
                {
                        midiPlayer.pause();
                        notifyPlayerStateObservers();
                }
        }

        public void stop()
        {
                if(!locked)
                {
                        midiPlayer.stop();
                        currentTick = 0;
                        notifyObservers();
                        notifyPlayerStateObservers();
                }
        }

        public void setLeadSheet(LeadSheet leadSheet)
        {
                this.leadSheet = leadSheet;
                if(leadSheet == null)
                {
                        if(midiPlayer != null && midiPlayer.getTrackCollection() != null)
                                midiPlayer.getTrackCollection().removeAllTracks();
                        initialiseInstruments();
                        notifyStateObservers();
                        return;
                }
                if(locked)
                        leadSheetWaiting = true;
                else
                        new SetLeadSheet(leadSheet).start();
        }

        private class SetLeadSheet extends Thread
        {
                private LeadSheet leadSheet;

                public SetLeadSheet(LeadSheet leadSheet)
                {
                        this.leadSheet = leadSheet;
                }
                /**
                 * Procedure being called when the thread is started.
                 */
                @Override
                public void run() {
                        locked = true;
                        clearObservables();
                        TrackCollection trackCollection = new TrackCollection(midiPlayer);
                        for(StaffCollection staffCollection : leadSheet)
                        {
                                for(Staff staff : staffCollection)
                                {
                                        boolean performanceStaff = false;
                                        for(Sequence sequence : staff.getSequences())
                                        {
                                                
                                                if(sequence.getTemplateClass() == Tone.class)
                                                {
                                                        performanceStaff = true;
                                                        sequence.setLockPerformance(false);
                                                }
                                                else if(sequence.getTemplateClass() == TimeSignature.class)
                                                {
                                                        sequence.setLockPerformance(false);
                                                        timeSignatureSequence = sequence;
                                                }
                                                else if(sequence.getTemplateClass() == Clef.class)
                                                        sequence.setLockPerformance(false);
                                        }
                                        if(performanceStaff)
                                        {
                                                staff.setTempoMap(leadSheet.getTempoMap());
                                                trackCollection.addTrack(new Track(staff));
                                        }
                                }
                                addObservable(staffCollection);
                        }
                        beatsPerSecond = leadSheet.getBPM() / 60.0;
                        currentTick = 0;
                        midiPlayer.setTrackCollection(trackCollection);
                        initialiseInstruments();
                        reloadInstrument();
                        notifyStateObservers();
                        addObservable(leadSheet);
                        locked = false;
                }
        }
        
        private void addObservable(ListObservable observable)
        {
                observable.addObserver(listObserver);
                listObservables.add(observable);
        }

        private void clearObservables()
        {
                for(ListObservable observable : listObservables)
                        observable.removeObserver(listObserver);
        }

        public void reloadInstrument()
        {
                setInstrument(leadSheet.getInstrument());
        }

        private void initialiseInstruments()
        {
                instrumentMap.clear();
                if(midiPlayer == null || midiPlayer.getTrackCollection() == null
                   || midiPlayer.getTrackCollection().getTrackCount() == 0
                   || midiPlayer.getTrackCollection().getInstruments() == null)
                        return;
                
                Instrument[] instruments = midiPlayer.getTrackCollection().getInstruments();
                for(int i = 0; i < instruments.length; i++)
                        instrumentMap.put(instruments[i].getName(), instruments[i]);
        }

        public void addStateObserver(Observer o)
        {
                stateObservable.addObserver(o);
        }

        public void removeStateObserver(Observer o)
        {
                stateObservable.removeObserver(o);
        }

        public void notifyStateObservers()
        {
                stateObservable.notifyObservers();
        }

        public void addPlayerStateObserver(Observer o)
        {
                playerStateObservable.addObserver(o);
        }

        public void removePlayerStateObserver(Observer o)
        {
                playerStateObservable.removeObserver(o);
        }

        public void notifyPlayerStateObservers()
        {
                playerStateObservable.notifyObservers();
        }

        public void timeUpdated(long time)
        {
                if(time >= 0)
                {
                        double beat = (time / 1000000000.0) * beatsPerSecond;
                        int ticksPerBeat = ModelConstants.denominatorToMusicalTime.get(4);
                        if(currentTick <= timeSignatureSequence.getMusicalLength())
                        {
                                ticksPerBeat = ModelConstants.denominatorToMusicalTime
                                        .get((((TimeSignature) timeSignatureSequence
                                        .getMusicalElement(currentTick)).getDenominator()));
                        }
                        currentTick = (int) (beat * ticksPerBeat);
                        notifyObservers();
                }
        }
}
